import {
	editor,
	Uri,
	IDisposable,
	languages,
	Position,
	CancellationToken,
	IEvent
} from './fillers/monaco-editor-core';

export interface WorkerAccessor<T> {
	(first: Uri, ...more: Uri[]): Promise<T>;
}

export type Diagnostic = editor.IMarkerData;

export interface IWorker {
	/**
	 * Check if there is any syntax or semantic error
	 * @param uri
	 */
	doValidation(uri: string): Promise<Diagnostic[]>;
	/**
	 * Get completions
	 * @param model
	 * @param position
	 * @param context
	 * @param token
	 */
	doComplete(uri: string, position: Position): Promise<languages.CompletionList | null>;
	/**
	 * Hover tips
	 * @param uri
	 * @param position
	 */
	doHover(uri: string, position: Position): Promise<languages.Hover | null>;
	/**
	 * Format the content
	 * @param uri
	 * @param range
	 * @param options
	 */
	format(
		uri: string,
		range: Range | null,
		options: languages.FormattingOptions
	): Promise<languages.TextEdit[]>;

	parserTreeToString(code: string): Promise<any>;
}

//NOTE Completion

export interface ILanguageWorkerWithCompletions {
	doComplete(uri: string, position: Position): Promise<languages.CompletionList | null>;
}

export class CompletionAdapter<T extends ILanguageWorkerWithCompletions>
	implements languages.CompletionItemProvider
{
	constructor(
		private readonly _worker: WorkerAccessor<T>,
		private readonly _triggerCharacters: string[]
	) {}

	public get triggerCharacters(): string[] {
		return this._triggerCharacters;
	}

	provideCompletionItems(
		model: editor.IReadOnlyModel,
		position: Position,
		context: languages.CompletionContext,
		token: CancellationToken
	): Promise<languages.CompletionList | undefined> {
		const resource = model.uri;

		return this._worker(resource)
			.then((worker) => {
				return worker.doComplete(resource.toString(), position);
			})
			.then((info) => {
				if (!info) {
					return;
				}
				return info;
			});
	}
}

//NOTE Diagnostics

export interface ILanguageWorkerWithDiagnostics {
	doValidation(uri: string): Promise<Diagnostic[]>;
}

export class DiagnosticsAdapter<T extends ILanguageWorkerWithDiagnostics> {
	protected readonly _disposables: IDisposable[] = [];
	private readonly _listener: { [uri: string]: IDisposable } = Object.create(null);

	constructor(
		private readonly _languageId: string,
		protected readonly _worker: WorkerAccessor<T>,
		configChangeEvent: IEvent<any>
	) {
		const onModelAdd = (model: editor.IModel): void => {
			let modeId = model.getLanguageId();
			if (modeId !== this._languageId) {
				return;
			}

			let handle: number;
			this._listener[model.uri.toString()] = model.onDidChangeContent(() => {
				window.clearTimeout(handle);
				handle = window.setTimeout(() => this._doValidate(model.uri, modeId), 500);
			});

			this._doValidate(model.uri, modeId);
		};

		const onModelRemoved = (model: editor.IModel): void => {
			editor.setModelMarkers(model, this._languageId, []);

			let uriStr = model.uri.toString();
			let listener = this._listener[uriStr];
			if (listener) {
				listener.dispose();
				delete this._listener[uriStr];
			}
		};

		this._disposables.push(editor.onDidCreateModel(onModelAdd));
		this._disposables.push(editor.onWillDisposeModel(onModelRemoved));
		this._disposables.push(
			editor.onDidChangeModelLanguage((event) => {
				onModelRemoved(event.model);
				onModelAdd(event.model);
			})
		);

		this._disposables.push(
			configChangeEvent((_) => {
				editor.getModels().forEach((model) => {
					if (model.getLanguageId() === this._languageId) {
						onModelRemoved(model);
						onModelAdd(model);
					}
				});
			})
		);

		this._disposables.push({
			dispose: () => {
				editor.getModels().forEach(onModelRemoved);
				for (let key in this._listener) {
					this._listener[key].dispose();
				}
			}
		});

		editor.getModels().forEach(onModelAdd);
	}

	public dispose(): void {
		this._disposables.forEach((d) => d && d.dispose());
		this._disposables.length = 0;
	}

	private _doValidate(resource: Uri, languageId: string): void {
		this._worker(resource)
			.then((worker) => {
				return worker.doValidation(editor.getModel(resource)?.getValue() || '');
			})
			.then((diagnostics) => {
				let model = editor.getModel(resource);
				if (model && model.getLanguageId() === languageId) {
					editor.setModelMarkers(model, languageId, diagnostics);
				}
			});
		// .then(undefined, (err) => {
		// 	console.error(err);
		// });
	}
}

//NOTE Hover

export interface ILanguageWorkerWithHover {
	doHover(uri: string, position: Position): Promise<languages.Hover | null>;
}

export class HoverAdapter<T extends ILanguageWorkerWithHover> implements languages.HoverProvider {
	constructor(private readonly _worker: WorkerAccessor<T>) {}

	provideHover(
		model: editor.IReadOnlyModel,
		position: Position,
		token: CancellationToken
	): Promise<languages.Hover | undefined> {
		let resource = model.uri;

		return this._worker(resource)
			.then((worker) => {
				return worker.doHover(resource.toString(), position);
			})
			.then((info) => {
				if (!info) {
					return;
				}
				return info;
			});
	}
}

//NOTE Formatting

export interface ILanguageWorkerWithFormat {
	format(
		uri: string,
		range: Range | null,
		options: languages.FormattingOptions
	): Promise<languages.TextEdit[]>;
}

export class DocumentFormattingEditProvider<T extends ILanguageWorkerWithFormat>
	implements languages.DocumentFormattingEditProvider
{
	constructor(private _worker: WorkerAccessor<T>) {}

	public provideDocumentFormattingEdits(
		model: editor.IReadOnlyModel,
		options: languages.FormattingOptions,
		token: CancellationToken
	): Promise<languages.TextEdit[] | undefined> {
		const resource = model.uri;

		return this._worker(resource).then((worker) => {
			return worker.format(resource.toString(), null, options).then((edits) => {
				if (!edits || edits.length === 0) {
					return;
				}
				return edits;
			});
		});
	}
}
